# Copyright (c) 2005,2006,2007 Dynamic Solutions Inc. (support@dynamic-solutions.com)
#
# This file is part of DataZoomer.
#
# DataZoomer is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# DataZoomer is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

import inspect

class FillException(Exception): pass

"""DataZoomer Template Filler"""

def replace_tag(expr,obj):
    tag  = expr.split(' ')[0]#.upper()
    rest = expr.split(' ')[1:]

    options = {}
    args = []
    for item in rest:
        opt = item.split('=')
        if len(opt)>1:
            value = opt[1].strip()
            if len(value)>1 and value[0]=='"' and value[-1]=='"':
                value = value[1:-1]
            if len(value)>1 and value[0]=="'" and value[-1]=="'":
                value = value[1:-1]
            options[opt[0].lower()] = value
        else:
            args.append(item.strip('""'))

    try:
        return '%s' % obj(tag,*args,**options)
    except:
        try:
            if isinstance(obj,(dict)) or 'get' in obj.__class__.__dict__:
            #if hasattr(obj,'get'):
                attr = obj.get(tag)
            else:
                attr = getattr(obj,tag)
        except AttributeError:
            attr = None

    if attr==None:
        result = '<#%s>' % expr
    elif inspect.ismethod(attr):
        result = attr(*args,**options) #'calling %s' % expr
    else:
        result = '%s' % attr

    return result


def fill_section(section,obj):
    def find_end(text,start):
        found = intext = newtag = escaped = deep = 0
        textch = ''

        for i in range(start,len(text)):
            c = text[i]

            if intext:
                escaped = (c == '\\')
                if (c == textch) and (not escaped): intext = 0

            else:
                if newtag:
                    if c == '#': deep += 1

                else:
                    if c in ['"',"'"]:
                        intext = 1
                        textch = c

                    elif c == '<':
                        newtag = 1

                    elif c == '>':
                        if deep > 0:
                            deep -= 1
                        else:
                            return i
        return -1

    start_tag = '<#'
    end_tag   = '>'
    start_tag_len = len(start_tag)

    crazyloop=0
    p1 = section.find(start_tag)
    while p1 > -1 and crazyloop<1000:
        crazyloop = crazyloop + 1
        p2 = find_end(section,p1+start_tag_len)
        if p2 > 0:
            expr = section[p1+start_tag_len:p2]
            repl = replace_tag(expr,obj)
            section = section.replace(start_tag+expr+end_tag,repl)
        else:
            return 'Nesting error in section'
        p1 = section.find(start_tag,p1+1)  # we don't know how long the section is now so perhaps the best we can do is start one character ahead of where we left off
    return section

def fill(text,obj):
    start_com = '<!--'
    end_com   = '-->'
    result = []

    p1 = text.find(start_com)
    p2 = 0
    while p1>-1:
        result.append(fill_section(text[p2:p1],obj))
        p2 = text.find(end_com,p1+len(start_com))
        if p2>-1:
            p2 += len(end_com)
            raw_text = text[p1:p2]
            result.append(raw_text)
            p1 = text.find(start_com,p2)
        else:
            p1 = -1
            p2 = len(text)

    result.append(fill_section(text[p2:],obj))
    return ''.join(result)


if __name__ == '__main__':
    import unittest

    def callback(tag_name):
        return tag_name.upper()

    class Filler: pass

    import dzentity
    class EntityFiller(dzentity.Entity):
        def __init__(self):
            dzentity.Entity.__init__(self)
            self.NAME = 'Fred'
        def get_ERR(self):
            raise FillException

    class FillTest(unittest.TestCase):

        def t(self,src,tar):
            context = Filler()
            context.NAME = 'Joe'
            context.SID  = 'mysid'
            context.FOOD = 'pizza'
            self.assertEqual(fill(src,context),tar)

        def t_by_line(self,src,tar):
            context = Filler()
            context.NAME = 'Joe'
            context.SID  = 'mysid'
            context.FOOD = 'pizza'
            t1 = tar
            t2 = fill(src,context)
            for s in zip(t1.splitlines(),t2.splitlines()):
                self.assertEqual(s[0],s[1])

        def test_text(self):
            self.t('test123','test123')

        def test_tag(self):
            self.t('test123<#SID>line2','test123mysidline2')
            self.t('test123<#SID>line2 <#NAME>','test123mysidline2 Joe')

        def test_exclusions(self):
            self.t('test <!-- exclude <#NAME> --> then <#SID> works','test <!-- exclude <#NAME> --> then mysid works')
            self.t('test <!-- exclude <#NAME> --> then <#SID> works','test <!-- exclude <#NAME> --> then mysid works')

        def test_multiline(self):
            self.t(\
"""Test
Hello <#NAME>
This is a test to see if <#FOOD>
is your favorite food.
""",
"""Test
Hello Joe
This is a test to see if pizza
is your favorite food.
""")

        def stest_multiline2(self):
            self.t_by_line(\
"""		<!-- end latest-post -->
		<!-- start recent-posts -->
		    <div id="recent-posts">
			    <div class="post">
                <#NAME>
			    <#SID>
			    <#SNIPPET name="sidebar">
				</div>
			</div>
""",
"""		<!-- end latest-post -->
		<!-- start recent-posts -->
		    <div id="recent-posts">
			    <div class="post">
                Joe
			    mysid
			    [#SNIPPET name="sidebar"]
				</div>
			</div>
""")
        def test_multiple(self):
            self.t('This <#NAME> is <#NAME> and <#SID> did match','This Joe is Joe and mysid did match')

        def test_missing(self):
            try:
                self.t('<#NAME>, the <#MISSING> tag is missing','Joe, the [#MISSING] tag is missing')
                self.assertEqual(fill('<#NAME>, the <#MISSING> tag is missing',EntityFiller()),'Fred, the <#MISSING> tag is missing')
            except:
                pass
            else:
                raise Exception, "Should have raised an exception"

        def test_callback(self):
            def set(name,*args,**keywords):
                print args, keywords
                if name=='SET':
                    return keywords.get('tpl','')

            t = 'Name is <#name>'
            self.assertEqual(fill(t,callback),'Name is NAME')
            d = dict(name='Joe')
            self.assertEqual(fill(t,d.get),'Name is Joe')

            d = dict(OTHERNAME='Joe')
            self.assertEqual(fill('Name is <#name "freddy">',d.get),'Name is freddy')
            d = dict(name='Joe')
            self.assertEqual(fill('Name is <#name "freddy">',d.get),'Name is Joe')

            t2 = fill('template is <#set tpl=one>',lambda name,*ar,**kw: name=='set' and kw.get('tpl','') or '')
            self.assertEqual(t2,'template is one')

        def test_entity(self):
            self.assertEqual(fill('Name is <#NAME>',EntityFiller()),'Name is Fred')

        def test_entity_attribute_missing(self):
            # in most classes, a missing attribute throws an error, in Entities, it resolves to None
            self.assertEqual(fill("<#NAME>'s addr is <#ADDR>",EntityFiller()),"Fred's addr is <#ADDR>")

        def test_class_attribute_missing(self):
            try:
                filler = Filler()
                filler.NAME = 'Joe'
                # in most classes, a missing attribute throws an error
                self.assertEqual(fill("<#NAME>'s addr is <#ADDR>",filler),"Joe's addr is [#ADDR]")
            except:
                pass
            else:
                raise Exception, "Should have raised an exception"

        def test_error_in_tag(self):
            try:
                fill('test <#ERR> error <#NAME>.',EntityFiller())
            except:
                pass
            else:
                raise Exception, "Should have raised an exception"

        def test_two_tag_bug(self):
            tpl = '<#first_name> <#last_name>'
            values = dict(first_name='al',last_name='smith')
            self.assertEqual(fill(tpl,values),'al smith')


    unittest.main()


